﻿using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using RayDen.Library.Components;
using RayDen.Library.Components.SystemComponents;
using RayDen.Library.Core.Primitives;
using RayDen.Library.Data.Import.OBJLoader;
using RayDen.Library.Data.Scene.SCFF;
using RayDen.Library.Entity;
using RayDen.Library.Entity.Frames;
using RayDen.Library.Entity.Interface;
using RayDen.Library.Entity.Renderer;
using RayDen.Library.Entity.Scene;
using RayDen.Library.Entity.SceneGraph;
using RayDen.RayEngine.Core.Interface;
using RayDen.RayEngine.Core.Library;
using RayDen.RayEngine.Data;
using RayDen.RayEngine.Engines.PathTracer.Volume;

using RayDen.RayEngine.Managers;
using RayDen.RayEngine.Samplers;
using RayDen.RayEngine.Scene;


namespace RayDen.RayEngine
{
    public class RayEngineConfiguration : IRenderConfig
    {

        public bool SingleFilm = true;
        public IRendererOutputSettings OutputSettings { get; set; }
        public IRendererQualitySettings QualitySettings { get; set; }
        public bool lowLatency = false;
        public bool AreaLightAsMeshLight = true;
        public bool EnableMaterialOverrides = false;
        public bool ImageMeasurement = false;
        public int RenderThreadsCount { get; set; }

        private DateTime start;

        public RenderingEngines engineType = GlobalConfiguration.Instance.Engine;
        public bool IsRunning { get; protected set; }

        public static MediumInfo Air = new MediumInfo() { IoR = 1.000277f, Attenuation = 0.1f };
        public static MediumInfo Glass = new MediumInfo() { IoR = 1.46f, Attenuation = 0.05f };

        protected int width, height;

        public BaseRenderingEngine engine;


        private DependencyManager dependencyManager;
        private DevicesManager deviceManager;
        private SceneBuilder _builder;
        private FrameManager frameManager;

        public Core.Interface.IImageFilm Film;


        public IImageFilm ImageFilm
        {
            get
            {
                if (this.SingleFilm)
                {
                    return FilmFactory.Cache;
                }

                var film = Film as ImageFilmBase;
                if (film == null)
                    return null;
                foreach (var renderThread in this.deviceManager.RenderThreads)
                {
                    film.Merge((ImageFilmBase)renderThread.Film);
                }

                return film;
            }
        }

        public RayEngineScene Scene;
        public ICamera Camera;
        public SurfaceSampler IntersectionManager;

        private SceneGeometryInfo scene;
        protected MaterialInfo[] mats;

        protected IParameterBag config;
        public SceneGraphInfo SceneGraph { get; protected set; }

        protected int currentCamera = 0;

        public string FrameName { get; set; }

        public long TotalSamples
        {
            get { return this.deviceManager.RenderThreads.Sum(renderThread => renderThread.sampler.TotalSamples); }
        }

        public int Pass
        {
            get { return this.deviceManager.RenderThreads.Sum(renderThread => renderThread.sampler.GetPass()); }
        }

        public RayEngineConfiguration()
        {
            this.config = new ConfigurableEntity(GlobalConfiguration.Instance);
            this.dependencyManager = new DependencyManager();
            //this.dependencyContainer.Get<>()

        }

        public virtual void Initialize()
        {
            this.engine = EngineFactory.CreateEngine(engineType, Scene, lowLatency);

            this.ImageMeasurement = config.Get("AdaptiveFilm", false);
            this.Film = FilmFactory.CreateFilm(width, height, this.ImageMeasurement);
            //this.Film = new CudaFilm(width, height);
            SamplerFactory.Instance.SetFilm(this.Film);
            this.deviceManager = new DevicesManager(Scene, engine) { SingleFilm = this.SingleFilm };
            this.frameManager = new FrameManager(this.OutputSettings, this.QualitySettings, Scene);
            this.deviceManager.Initialize(this.RenderThreadsCount, width, height, frameManager);
            this.frameManager.SetDevices(deviceManager, Film);
            //this.frameManager.OnFrameReady += (fm) =>
            //    {
            //        this.frameManager
            //    };
            Tracer.TraceLine("Starting [{0}] ( Preprocessing : {1} )", engineType, engine.PreprocessRequired);
            Tracer.TraceLine("..Sampler Type [{0}]", deviceManager.RenderThreads.First().sampler.GetType().Name);
            Tracer.TraceLine("..Image Film Type [{0}]", Film.GetType().Name);


            if (engine.PreprocessRequired)
            {
                engine.Preprocess(Scene, deviceManager.IntersectionDevice);
            }
        }
        public void RunCommand(int index)
        { }

        public void OpenFrame(FrameDescription frame)
        {
            this.config.Merge(frame);
            this.OutputSettings = new OutputSettingsInfo(frame);
            this.QualitySettings = new QualitySettingsInfo(frame);

            this.FrameName = frame.FrameName;
            width = frame.Get<int>("ImageWidth");
            height = frame.Get<int>("ImageHeight");

            if (!string.IsNullOrWhiteSpace(frame.WorkingDir))
                Directory.SetCurrentDirectory(frame.WorkingDir);
            /*
            foreach (var frameElement in frame.Elements.Where(frameElement => frameParsers.ContainsKey(frameElement.GetType()))) {
                frameParsers[frameElement.GetType()](frameElement);
            }*/

            if (Scene == null)
            {
                SceneGraphService svc = new SceneGraphService();
                svc.Populate(
                    //new ObjLoader(),
                    new GlobalIndexObjLoader(),
                    new MaterialLoader());

                //svc.OpenFrame(frame);
                _builder = new SceneBuilder() { SGService = svc };
                _builder.Setup();
                this.SceneGraph = _builder.SGService.SceneGraph;
                this.Scene = _builder.OpenFrame(frame, this.AreaLightAsMeshLight);
                this.Scene.DefaultLightGain = config.Get("LightGain", new RgbSpectrum(1.0f));
                this.Scene.MaxPathDepth = config.Get("MaxPathDepth", 32);
                this.Scene.RussianRuletteImportanceCap = config.Get("RRImportanceCap", 0.75f);
                this.Scene.MaxPaths = config.Get("PathBufferSize", 65536);
                this.Scene.ShadowRayCount = config.Get("ShadowRayCount", 1);
                this.Scene.TextureSamplingQuality = this.QualitySettings.TextureSamplingQuality;
                this.Scene.LightSamplingStrategy = this.QualitySettings.LightSamplingStrategy;
                RgbSpectrum.Gamma = true;
                /*
                if (GlobalConfiguration.Instance.SpectralRendering)
                {
                    this.Scene.SpectralMats = new BxDFLibrary(new LambertianBrdf(new[] { 0.5f, 0.5f, 0.5f }));
                    this.Scene.SpectralMats.Populate(_builder.MaterialInfos.ToArray());
                }
                else*/

                {
                    this.Scene.MatLib = this.dependencyManager.GetMaterialPlugin(_builder.MaterialInfos.ToArray());
                    if (_builder.FrameMaterialBuilder != null)
                    {
                        this.Scene.MatLib.Add(_builder.FrameMaterialBuilder.Materials);
                        _builder.FrameMaterialBuilder.CreateRayEngineMaterials(this.Scene);
                    }
                    if (EnableMaterialOverrides)
                        FrameMaterialBuilder.ApplyMaterialOverrides((this.Scene.MatLib as OptimizedBsdfLibrary).Materials, Scene);
                }


                RenderThreadsCount = config.Get("RenderThreadsCount", 1);
                this.scene = _builder.SceneGeo;

                var sceneBound = new AABB(scene.BoundingSphereCenter - scene.BoundingSphereRadius,
                                          scene.BoundingSphereCenter + scene.BoundingSphereRadius);
                sceneBound.Expand(1f);

                //var viewBound = new AABB(Scene.Camera.Target - 5f, Scene.Camera.Target + 5f);
                var c = new Point(Scene.Camera.Target);
                var bb = new AABB(c - 1f, c + 1f);
                var volumeBound = sceneBound;

                Scene.VolumeIntegrator = new SingleScatteringIntegrator(volumeBound,
                                                                         volumeBound.Size.Length / 10f, //step
                                                                         0.05f, //Probability
                                                                         RgbSpectrum.UnitSpectrum() * 0.05f,  //Inscatering
                                                                         new RgbSpectrum(0.000f)             //Emission

                                                                       );
                new RayEngineMaterialBuilder().BuildMaterials(this.Scene);
            }

            this.Initialize();
            ExportConfiguration(frame).Save(@"F:\Dev\Frames\Config\" + Path.GetFileNameWithoutExtension(FrameName) + ".sceneconfig");
        }


        public SceneConfiguration ExportConfiguration(FrameDescription frame)
        {
            var result = new SceneConfiguration();

            result["frame", "global", "name"] = FrameName;
            result["frame", "global", "width"] = width;
            result["frame", "global", "height"] = height;
            result["frame", "global", "directory"] = Directory.GetCurrentDirectory();

            result.SetFrame("samplesperpixel", QualitySettings.SamplesPerPixel);
            result.SetFrame("supersampling", QualitySettings.SuperSamplingSize);
            result.SetFrame("maxpathdepth", Scene.MaxPathDepth);
            result.SetFrame("lightgain", this.Scene.DefaultLightGain);
            result.SetFrame("rrimportancecap", this.Scene.RussianRuletteImportanceCap);
            result.SetFrame("shadowraysperlight", this.Scene.ShadowRayCount);
            result.SetFrame("texturesampling", (int)this.QualitySettings.TextureSamplingQuality);
            result.SetFrame("pathbuffersize", this.Scene.MaxPaths);
            result.SetFrame("arealightasmesh", this.AreaLightAsMeshLight);

            /*
            foreach (var frameElement in frame.Elements.Where(frameElement => frameParsers.ContainsKey(frameElement.GetType()))) {
                frameParsers[frameElement.GetType()](frameElement);
            }*/

            if ((Scene.Camera is FiniteAppertureCamera))
            {
                result.FiniteAppertureCameras.AddItem("camera1", new CameraFactory().Serialize(Scene.Camera), ".");
            }
            else
            {
                result.PinholeCameras.AddItem("camera1", new CameraFactory().Serialize(Scene.Camera), ".");
            }


            var objTag = frame.Elements.OfType<FrameObjFileReference>().FirstOrDefault();
            result.SetMesh("objfilename", objTag.ObjFilePath);
            result.SetMesh("mtlfilename", objTag.MtlFilePath);
            result.SetMesh("invertnormals", objTag.InvertNormals);

            var serializer = new LightFactory();
            foreach (var light in Scene.Lights)
            {
                EntityCollection lightCollection = null;


                if (light is PointLight)
                {
                    lightCollection = result.PointLights;

                }
                else if (light is MeshLight)
                {
                    lightCollection = result.AreaLights;
                }
                else if (light is BaseInfiniteLight)
                {
                    lightCollection = result.InfiniteLights;
                }

                if (lightCollection != null)
                    lightCollection.AddItem(light.Name, serializer.Serialize(light), ".");
            }

            /*
            var matSer = new MaterialFactory();

            foreach (var material in Scene.MatLib)
            {
                EntityCollection mats = null;
                switch (material.Class)
                {
                    case BrdfClass.Alloy:
                        mats = result.AlloyMaterials;
                        break;
                    case BrdfClass.DiffuseLambert:
                        mats = result.MatteMaterials;
                        break;

                }
                if (mats != null)
                {
                    var mat = matSer.Serialize(material);
                    mats.AddItem(mat.Name, mat,".");
                }
            }

            */
            return result;
        }

        public void OpenSceneConfiguration(string configPath)
        {
            Scene = new RayEngineScene();
            this.Scene.DefaultLightGain = config.Get("LightGain", new RgbSpectrum(1.75f));
            this.Scene.MaxPathDepth = config.Get("MaxPathDepth", 10);
            this.Scene.RussianRuletteImportanceCap = config.Get("RRImportanceCap", 0.75f);
            this.Scene.MaxPaths = config.Get("PathBufferSize", 65536 >> 1);
            this.Scene.ShadowRayCount = config.Get("ShadowRayCount", 1);

            var loader = new SceneConfigurationLoader();
            loader.Load(configPath, Scene);
            this.width = ValueReader.Convert<int>(loader.Config.ReadFrame("width").ToString());
            this.height = ValueReader.Convert<int>(loader.Config.ReadFrame("height").ToString());
            this.AreaLightAsMeshLight = Scene.AreaLightAsMesh;

            this.Scene.MatLib = this.dependencyManager.GetMaterialPlugin(loader.MaterialInfos);
            if (loader.Materials.Any())
            {
                this.Scene.MatLib.Merge(loader.Materials.ToArray());
            }

            RenderThreadsCount = config.Get("RenderThreadsCount", 1);
            this.scene = loader.SceneGeo;

            var sceneBound = new AABB(scene.BoundingSphereCenter - scene.BoundingSphereRadius,
                                        scene.BoundingSphereCenter + scene.BoundingSphereRadius);
            sceneBound.Expand(1f);

            var volumeBound = sceneBound;
            this.FrameName = loader.Config.ReadFrame("name").ToString();

            Scene.VolumeIntegrator = new SingleScatteringIntegrator(volumeBound,
                                                                        volumeBound.Size.Length / 10f, //step
                                                                        0.05f, //Probability
                                                                        RgbSpectrum.UnitSpectrum() * 0.05f,  //Inscatering
                                                                        new RgbSpectrum(0.0005f)             //Emission

                                                                    );
            foreach (var triangleMeshInfo in Scene.sceneData.Meshes)
            {
                triangleMeshInfo.MaterialID = ((MaterialInfoContainer)Scene.MaterialProvider).GetMaterialID(triangleMeshInfo.MaterialName);
            }

            this.QualitySettings = loader.qualitySettings;
            this.OutputSettings = loader.outputSettings;
            this.Initialize();
        }


        public void BuildNextFrame()
        {


        }

        public void NextCamera()
        {
            if (currentCamera < Scene.Cameras.Count)
            {
                currentCamera++;
                this.Camera = Scene.Cameras[currentCamera];
                Scene.Camera = (BaseCamera)this.Camera;
            }
        }

        public void PrevCamera()
        {
            if (currentCamera > 0)
            {
                currentCamera--;
                this.Camera = Scene.Cameras[currentCamera];
                Scene.Camera = (BaseCamera)this.Camera;
            }
        }



        public void StartRenderer()
        {
            IsRunning = true;
            start = DateTime.UtcNow;
            this.RenderThreadsCount = this.deviceManager.RenderThreads.Length;
            this.deviceManager.Start();
        }

        public string GetStats()
        {
            long samps = 0;
            foreach (var renderThread in deviceManager.RenderThreads)
            {
                samps += renderThread.sampler.TotalSamples;
            }
            var totalSamples = samps;
            //this.deviceManager.RenderThreads.Sum(renderThread => renderThread.sampler.TotalSamples);

            var samplesPerSec = totalSamples / (float)(DateTime.UtcNow - start).TotalSeconds;

            var rays = (this.deviceManager.IntersectionDevice.TotalRays / (DateTime.UtcNow - start).TotalSeconds) / 1000000d;
            return string.Format("{4:F5} MRays/sec {0:F5} MSamples/sec ; {1} Elapsed ; {2} Samples Per Pixel ; {3} ; {5};",
                //{0:F5} MRays/sec  this.IntersectionDevice.GetPerformance() / 1000000d, 
                samplesPerSec / 1000000d, DateTime.UtcNow - start, Pass, engine.GetEngineStats(), rays, Film.GetStats()
                //_renderThreads[0].sampler.GetPass(), 
                //Engine.GetStats()
                );
        }

        public void Save(string fn)
        {
            (this.Film).SavePNG(fn, GetStats().Split(';').Select(item => item.Trim()).Union(new[] { string.Format("Engine - {0}", this.engineType), SamplerFactory.Instance.GetSamplerType() }).ToArray());
        }

        public void Dispose()
        {
            if (this.deviceManager != null)
            {
                this.deviceManager.Dispose();
            }
            if (Film is IDisposable)
            {
                (Film as IDisposable).Dispose();
            }
        }


        public void Interrupt()
        {
            this.deviceManager.Interrupt();
        }

        #region IRenderConfig Members

        public string SavePath
        {
            get { return config.Get("ImageSaveDirectory", @"F:\Dev\Images\MovieFactory\"); }
        }
        public int Width
        {
            get { return width; }
        }

        public int Height
        {
            get { return height; }
        }

        public void SaveImage(string fn)
        {
            this.Save(fn);
        }

        public void SaveRawImage(string fn)
        {
            this.Film.SaveRaw(fn);
        }

        public void Start()
        {
            this.StartRenderer();
        }

        public IImagePlaneComponent GetImagePlane()
        {
            return (this.ImageFilm);
        }


        public virtual void ConsumeJob(IRenderJob job, Action<IRenderJobResult> resultCallback)
        {
            throw new NotImplementedException();
        }

        #endregion


        public void CheckRestart()
        {
            if (this.deviceManager.RenderThreads.All(item => !item.Running))
            {
                this.frameManager.SetupNextFrame();
            }
        }

        public void ResetFilm()
        {
            this.Film.Reset();
            this.Film.UpdateScreenBuffer();
        }
    }
}
